iT邦幫忙

2026 iThome 鐵人賽

DAY 29
0
自我挑戰組

React 入門到實作與除錯|30 天哩ㄟ刻系列 第 29 篇

Day 29 : 蹭熱度,做個需求顧問 AI 網站之三

  • 分享至 

  • xImage
  •  

把成果帶走,再開始下一次討論。

昨天把表單、API 和 AI 串起來,今天接著補上複製成果與重新討論,再從頭確認操作和整理出來的內容。

複製討論稿

問題摘要、候選需求與待確認事項,這三個是主要的內容,把討論背景和三輪問答一起放進去,在討論的時候能更清楚當時的理由是什麼,接著我們來新增複製討論稿的功能:

新增 app/shared/discussion-text.ts,用一般函式把資料整理成有標題、清單與換行的文字。以下節錄候選需求的部分,完整函式放在文末附錄:

// 候選需求
const requirements = result.requirements.length === 0
  ? ["資訊尚不足以形成候選需求,請查看待確認事項。"]
  : result.requirements.flatMap((item, index) => [
    `### ${index + 1}. ${item.content}`,
    `理由:${item.reason}`,
    `User Story:${item.userStory}`,
    "建議驗收條件:",
    ...item.acceptanceCriteria.map((criterion) => `- ${criterion}`),
    "",
  ]);

上面片段負責把傳入的資料轉成文字,最後用 join("\n") 接成一整個段落。

回到 app/components/requirements-assistant.tsx,匯入函式:

import { formatDiscussionDraft } from "../shared/discussion-text";

在外層 RequirementsAssistant 裡,從已完成的 session 計算 discussionText,再加入複製事件:

type CopyState = { status: "idle" | "pending" | "success" | "error" };

// 放在 RequirementsAssistant 內。
const [copy, setCopy] = useState<CopyState>({ status: "idle" });
const [resetOpen, setResetOpen] = useState(false);
const copying = useRef(false);
const copyPending = copy.status === "pending";
const discussionText = session.stage === "result"
  ? formatDiscussionDraft(session.background, session.history, session.result) : "";

async function handleCopy() {
  if (copying.current || resetOpen || session.stage !== "result") return;
  copying.current = true;
  setCopy({ status: "pending" });
  try {
    if (!navigator.clipboard?.writeText) throw new Error("瀏覽器不支援自動複製。");
    await navigator.clipboard.writeText(discussionText);
    setCopy({ status: "success" });
  } catch {
    setCopy({ status: "error" });
  } finally {
    copying.current = false;
  }
}

說明:

  • discussionText 從訪談結果計算出來。
  • copy 用來顯示複製狀態。
  • copying.current 則在事件裡立即擋住重複操作。
  • resetOpen 是確認視窗的開關。

複製用瀏覽器的 navigator.clipboard.writeText() 功能,等 Promise 成功才顯示成功提示。Clipboard API 需要安全的執行環境,正式網站通常使用 HTTPS,本機開發的 localhost 也可視為可信任來源,瀏覽器可能會拒絕寫入,所以需要處理失敗。

MDN:writeText
MDN:Secure contexts

在結果區塊下方接上複製按鈕:

<Button type="button" variant="contained" onClick={handleCopy} disabled={copyPending}>
  {copyPending ? "複製中…" : "複製討論稿"}
</Button>
{copy.status === "success" ? (
  <Alert severity="success" role="status">已複製討論稿,包含背景與訪談紀錄。</Alert>
) : null}

按鈕放在 session.stage === "result" 的區塊內,與 DraftResult 一起顯示,事件onClick={handleCopy},讓使用者點擊才執行。

React:Responding to Events

自動複製失敗怎麼辦

複製失敗的時候顯示同一份文字,讓使用者手動全選複製:

{copy.status === "error" ? (
  <Stack spacing={2}>
    <Alert severity="warning">自動複製失敗,請在下方文字框全選後手動複製。</Alert>
    <TextField
      id="manual-copy" label="手動複製討論稿" multiline minRows={8} maxRows={16} fullWidth
      value={discussionText} slotProps={{ input: { readOnly: true } }}
      helperText="點入文字框,按 Ctrl+A 或 Command+A 全選,再按 Ctrl+C 或 Command+C 複製。"
    />
  </Stack>
) : null}

這個欄位使用 readOnly,保留點選、選取與複製的能力。

重新開始討論

接著加上重新開始,目標是清空背景、當前回答、問答紀錄、結果和狀態提示,回到最初的表單。

要清掉的資料都在外層 RequirementsAssistant,所以我們把重設事件也放在這裡,內層元件會收到新的 props,跟著顯示空白表單。

function handleRequestReset() {
  if (sending.current || copying.current) return;
  setResetOpen(true);
}

function handleReset() {
  if (sending.current || copying.current) return;
  setBackground({ problem: "", audience: "", currentProcess: "" });
  setSession({ stage: "background" });
  setAnswerDraft("");
  setRequest({ status: "idle" });
  setCopy({ status: "idle" });
  setResetOpen(false);
}

如果只把 session.stage 改回 "background",這樣背景草稿還是舊的,複製提示也可能留下來。所以重新開始要同時檢查本次討論有哪些資料。

重設前我們用 MUI 的 Dialog 彈出一個確認視窗,讓使用者有機會取消、先複製成果:

const hasData = session.stage !== "background"
  || Object.values(background).some((value) => value.length > 0);

<Button type="button" variant="outlined" onClick={handleRequestReset}
  disabled={pending || copyPending || !hasData}>
  重新開始
</Button>

<Dialog open={resetOpen} onClose={() => setResetOpen(false)}
  aria-labelledby="reset-title" aria-describedby="reset-description">
  <DialogTitle id="reset-title">重新開始這次討論?</DialogTitle>
  <DialogContent>
    <DialogContentText id="reset-description">
      將清空背景、回答、問答紀錄與討論稿。需要保留成果時,請先取消並複製討論稿。
    </DialogContentText>
  </DialogContent>
  <DialogActions>
    <Button type="button" onClick={() => setResetOpen(false)} autoFocus>取消</Button>
    <Button type="button" onClick={handleReset} disabled={pending || copyPending}>清空並重新開始</Button>
  </DialogActions>
</Dialog>

hasData 放在元件的 return 前,按鈕與 Dialog 放在回傳的 JSX,按取消關閉視窗,按確認執行重設。

MUI:Dialog

API 等待或複製尚未完成時,暫時不能重新開始。這樣可以先讓當前操作結束,再清空資料,避免舊請求回來後又把畫面切回訪談。

這次改用免費模型

昨天範例使用的 google/gemini-2.5-flash 是付費模型,我們改用 OpenRouter 的免費模型路由,在專案根目錄的 .env 修改:

OPENROUTER_MODEL=openrouter/free

openrouter/free 會依請求需要的能力,從可用的免費模型中選擇。本次保留 JSON 模式與 provider: { require_parameters: true },伺服器仍檢查回應格式與輪次,實際使用的模型與回答可能不同。

OpenRouter:Free Models Router

免費模型仍需要有效金鑰,也有請求次數限制。完整三輪訪談會呼叫模型四次,重試也可能占用次數,測試時記得確認自己的帳戶限制,修改 .env 後要重新啟動伺服器。

OpenRouter:用量限制

從頭操作一次

完整程式放在最後,在專案根目錄執行 npm run dev,開啟終端機顯示的網址。

接著我們用交班需求案例測試,畫面中的問題與結果是 OpenRouter 免費模型實際回傳,題目每次可能不同。

欄位 內容
想解決的問題 想讓店裡交班不要漏事情,第一版先不做庫存自動扣除
誰會用 早班與晚班店員,偶爾由店長協助確認
目前如何處理 口頭交班,沒有留下紀錄,忙的時候常漏掉待辦事項

按「開始訪談」,依當次問題回答:不知道的責任人、權限或原因,直接寫不知道,三輪回答完成後,畫面切換成需求討論稿,不再多問第四題。

在 Chrome 開發者工具的 Network 篩選 clarify,可查看每次送出的 Payload:

操作 history 筆數 成功後的畫面
開始訪談 0 第 1 / 3 題
回答第一題 1 第 2 / 3 題
回答第二題 2 第 3 / 3 題
回答第三題 3 需求討論稿

正常完成一次訪談共四次模型請求,三輪是三個回答。結果的 Response 應是 kind: "result",包含摘要、候選需求與待確認事項。

實際 AI 討論稿與 Chrome 開發者工具

上圖是實際 AI 討論稿與開發者工具,藉由 /api/clarify 的回應核對結果階段。

失敗後能不能接著做

第二題填好回答後,在 Network 暫時選 Offline,再按送出。這次實測失敗後同一題、回答草稿與已完成的第一輪紀錄都保留。恢復 No throttling 後重試,送出的 history 仍是兩筆,成功後才把第二輪加入畫面紀錄。

advance() 會組合本次要送出的資料,拿到有效回應後才更新 session.history、清空回答,所以失敗的回答不會重複追加成已完成的問答。

複製成果,再重新開始

討論稿出現後按「複製討論稿」,等成功提示出現,再貼到文字編輯器。檢查複製文字,應該要包含背景、三輪問答、摘要、需求理由、User Story、建議驗收條件與待確認事項。

複製討論稿成功

上圖保留實際討論稿與複製成功提示。

接著按「重新開始」。如果按取消,結果與複製提示會保留,當再開啟確認視窗,按「清空並重新開始」,背景、回答、紀錄、結果與提示一起清空,回到初始表單。

重新開始確認視窗

上圖是清空前的確認視窗,提醒需要保留成果的人先取消並複製。


這個小專案從背景表單開始,讓 AI 追問三輪,再整理成可以帶走的討論稿。受控輸入、state、props、事件、陣列更新和 MUI,都用在這個流程裡。

明天回到這三十天的學習,整理我們學習處理哪些問題,以及哪些觀念。

參考資料

附錄 完整程式

https://github.com/hy9811/ai-requirements-assistant


上一篇
Day 28 : 蹭熱度,做個需求顧問 AI 網站之二
系列文
React 入門到實作與除錯|30 天哩ㄟ刻 共 29 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言